// Note that this Channel class does NOT include methods for Encoded
// Upload and Encoded Download. That class will be provided later.
// When you receive it, please delete the old class and use only the
// new one.

// This class implements a channel that can be written to and read
// from by all parts of the P2P system: Directory, PeerClient,
// PeerServer, Repository

package fournitures;

import java.io.*;
import java.net.*;

public class Channel {

	// The socket that is associated with this side of the Channel:
	public Socket socket = null;

	// The streams in/out are used to read/write to/from the channel:
	public ObjectInputStream in = null;
	public ObjectOutputStream out = null;

	// Default timeout value (in milliseconds):
	public final static int DEFAULTTIMEOUT = 5000;

	// Whether debugging messages should be displayed for this channel:
	public boolean displayDebuggingMessages = false;

	// The following two names are used in order to make debugging
	// messages easier to read. Name1 is the process that created this
	// Channel, name2 is the process that it connected to.
	public String name1;
	public String name2;

	// ----------------------------------------------------------------------
	// Constructors
	// ----------------------------------------------------------------------

	public Channel(String address, int portNumber, int timeout,
			boolean displayDebuggingMessages, String name)
			throws ConnectionTimeoutException, ConnectionRefusedException,
			FatalChannelException {

		// Calling this constructor signals that the caller
		// wants to connect to the given address and port. It
		// will wait until the other side replies. If the
		// connection is refused, a ConnectionRefusedException
		// is thrown. If the connection takes more than
		// timeout milliseconds to establish, a
		// ConnectionTimeoutException is thrown. If the
		// timeout parameter is negative, a default value of
		// 5000 milliseconds is used. A zero timeout value is
		// interpreted as an infinite timeout which means the
		// program will wait indefinitely for a connection
		// (see the API documentation of the setSoTimeout
		// method. A refused or timed-out exception is usually
		// not a fatal error and the program can continue if
		// the exception is caught by the calling method. If
		// the connection cannot be established for some other
		// reason, a FatalChannelException is thrown. This
		// usually signals a serious error and the program
		// should be terminated.

		// Set up the names for debugging messages:
		this.displayDebuggingMessages = displayDebuggingMessages;
		name1 = name;
		if (displayDebuggingMessages) {
			System.out.println("     Channel created by " + name1);
			System.out.println("     Channel " + name1
					+ ": establishing a connection to " + address + ", port "
					+ portNumber);
		}

		// Make sure the timeout value is reasonable. A value of 0
		// means timeout = infinity (no timeout)
		if (timeout < 0)
			timeout = DEFAULTTIMEOUT;

		try {
			// Create a socket that can bind to the address and port
			// and set the timeout value. If the connection is
			// refused, a java.net.ConnectException is thrown. To be
			// able to set a timeout here, we have to create an unconnected
			// socket first and then connect the socket by indicating the
			// desired
			// timeout value.
			socket = new Socket();
			SocketAddress sockaddr = new InetSocketAddress(address, portNumber);
			socket.connect(sockaddr, timeout);

			// The timeout specifies how long a receive call will
			// block. If one of the receive methods is called and
			// doesn't receive anything during "timeout" milliseconds,
			// a ConnectionTimeoutException is thrown.
			socket.setSoTimeout(timeout);

			// Set up the in/out streams for reading and writing. If
			// this takes too long, a ConnectionTimeoutException is
			// thrown.
			setupStreams();

			// Exchange names between the two channels in order to
			// improve later debugging messages. Temporarily disable
			// debugging messages during this exchange as it clutters
			// up the screen.
			boolean keep = this.displayDebuggingMessages;
			this.displayDebuggingMessages = false;
			this.sendString(name1);
			// Then receive the name of the other process:
			name2 = this.receiveString();
			this.displayDebuggingMessages = keep;
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1
						+ ": connection established to " + name2);
			}
		}

		catch (java.net.SocketTimeoutException e) {
			// The connection was not refused but it took longer than
			// timeout milliseconds to create the connection. This is
			// not necessarily a fatal error so we throw a
			// ConnectionTimeoutException to the caller of this method
			// that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection timeout while connecting socket.");
			}
			ConnectionTimeoutException ce = new ConnectionTimeoutException(e);
			throw (ce);
		} catch (java.net.ConnectException e) {
			// The connection was refused. This is not necessarily a
			// fatal error so we throw a ConnectionRefusedException to
			// the caller of this method that can catch it and take
			// some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection refused");
			}
			ConnectionRefusedException ce = new ConnectionRefusedException(e);
			throw (ce);
		} catch (ConnectionTimeoutException e) {
			// The connection was not refused but it took longer than
			// timeout milliseconds to create the connection. This is
			// not necessarily a fatal error so we throw a
			// ConnectionTimeoutException to the caller of this method
			// that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection timeout");
			}
			throw (e);
		} catch (Exception e) {
			// Some other problem occurred while setting up the
			// connection. This is considered a fatal error and the
			// program is terminated.
			System.out
					.println("Channel: an error occurred while trying to connect to "
							+ address + ", port " + portNumber);
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public Channel(Port port, boolean displayDebuggingMessages, String name)
			throws FatalChannelException {

		// Calling this constructor signals that the caller wants to
		// wait for connections to the given port, alternatively pick
		// up a connection that is already waiting. The constructor
		// does not return until a connection is established. If there
		// is a problem, a FatalChannelException is thrown. This is
		// usually a signal of a serious error and the program should
		// be terminated and corrected.

		// Set up the names for debugging messages:
		this.displayDebuggingMessages = displayDebuggingMessages;
		name1 = name;
		if (displayDebuggingMessages) {
			System.out.println("     Channel created by " + name1);
			System.out.println("     Channel " + name1
					+ ": waiting for somebody to connect to port "
					+ port.portNumber);
		}

		try {
			// Accept a connection, i.e. either pick up a waiting
			// connection or wait until somebody connects. Note that
			// the accept method blocks until a connection is
			// available.
			socket = port.serverSocket.accept();

			// Set up the in/out streams for reading and writing:
			setupStreams();

			// Exchange names between the two channels in order to
			// improve later debugging messages. Temporarily disable
			// debugging messages during this exchange as it clutters
			// up the screen.
			boolean keep = this.displayDebuggingMessages;
			this.displayDebuggingMessages = false;
			// The other connecting Channel immediately sends its name:
			name2 = this.receiveString();
			// Then this channel should send its name:
			this.sendString(name1);
			this.displayDebuggingMessages = keep;

			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1
						+ ": connection established from " + name2);
			}
		}

		catch (Exception e) {
			System.out
					.println("Channel: an error occurred while waiting for somebody to connect to "
							+ port.portNumber);
			// Close the sockets and streams before returning a
			// FatalChannelException:
			System.out.println("Channel: close the socket and streams");
			try {
				if (in != null)
					in.close();
				if (out != null)
					out.close();
				socket.close();
			} catch (java.io.IOException e2) {
				System.out
						.println("Channel: failed to close the socket or the streams");
				FatalChannelException ce = new FatalChannelException(e2);
				throw (ce);
			}
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public void setupStreams() throws ConnectionTimeoutException,
			FatalChannelException {
		// This method is called when a socket has been created. It
		// sets up the in/out streams for the channel. A timeout may
		// occur when the input stream is created and in that case a
		// ConnectionTimeoutException is thrown. If there is any
		// other problem, a FatalChannelException is thrown.

		try {
			// Get the general in/out streams that are already
			// associated with the socket:
			InputStream generalIn = socket.getInputStream();
			OutputStream generalOut = socket.getOutputStream();

			// We want to write bytes, ints, Strings and objects of
			// type Packet to the channel. The most appropriate filter
			// stream is thus Object.
			// Don't forget to let the objects written to the stream
			// implement java.io.Serializable.

			// Create the object output stream first and flush it
			// before creating the input stream, otherwise the input
			// constructor may block (see API documentation). This is
			// necessary only for Object streams and does not apply
			// to, for example, Data streams.
			out = new ObjectOutputStream(generalOut);
			out.flush();

			// Create the object input stream. If this takes too long,
			// the other party has probably disappeared. This will be
			// detected as a timeout according to the SO_TIMEOUT
			// property and a java.net.SocketTimeoutException will be
			// thrown.
			in = new ObjectInputStream(generalIn);
		} catch (java.net.SocketTimeoutException e) {
			// The connection was not refused but it took longer than
			// timeout milliseconds to create the connection. This is
			// not necessarily a fatal error so we throw a
			// ConnectionTimeoutException to the caller of this method
			// that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out
						.println("     Channel "
								+ name1
								+ "/"
								+ name2
								+ ": connection timeout while setting up the in/out streams.");
			}
			ConnectionTimeoutException ce = new ConnectionTimeoutException(e);
			throw (ce);
		} catch (Exception e) {
			// Some other error occurred. Throw a FatalChannelException.
			System.out
					.println("Channel: an error occurred while setting up the in/out streams.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	// ----------------------------------------------------------------------
	// Send/receive methods
	// ----------------------------------------------------------------------

	public void sendString(String message) throws FatalChannelException {
		// This method sends the given message over the channel as a
		// String. If there is a problem, e.g. if the connection is
		// closed by the other side, a FatalChannelException is thrown.

		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": sending String " + message);
		}

		try {
			out.writeUTF(message);
			out.flush(); // Don't forget to flush the output stream after
							// writing!
		}

		catch (Exception e) {
			System.out
					.println("Channel: an error occurred while sending the message.");
			// e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public String receiveString() throws ConnectionTimeoutException,
			FatalChannelException {
		// This method waits for a message in the form of a String and
		// returns it. If there is a problem, e.g. if the connection is
		// closed by the other side, a FatalChannelException is thrown.
		String message = null;
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": waiting for a String ...");
		}
		try {
			message = in.readUTF();
		} catch (EOFException e) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": (EOF) read operation interrupted, "
					+ "one side of the socket was probably closed");
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		} catch (java.net.SocketTimeoutException e) {
			// The read() operation waited longer than timeout milliseconds
			// without receiving anything. This is not necessarily a fatal
			// error so we throw a ConnectionTimeoutException to the caller
			// of this method that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection timeout");
			}
			ConnectionTimeoutException ce = new ConnectionTimeoutException(e);
			throw (ce);
		} catch (java.net.SocketException e) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": (SocketException) read operation interrupted, "
					+ "one side of the socket was probably closed");
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		} catch (Exception e) {
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": received String " + message);
		}
		return message;
	}

	public void sendInt(int i) throws FatalChannelException {
		// Analogous to sendString but for int.

		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": sending int " + i);
		}
		try {
			out.writeInt(i);
			out.flush(); // Don't forget to flush the output stream after
							// writing!
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while sending the message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public int receiveInt() throws ConnectionTimeoutException,
			FatalChannelException {
		// Analogous to receiveString but for int.

		int i = 0;
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": waiting for an int ...");
		}
		try {
			i = in.readInt();
		} catch (java.net.SocketTimeoutException e) {
			// The read() operation waited longer than timeout milliseconds
			// without receiving anything. This is not necessarily a fatal
			// error so we throw a ConnectionTimeoutException to the caller
			// of this method that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection timeout");
			}
			ConnectionTimeoutException ce = new ConnectionTimeoutException(e);
			throw (ce);
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while waiting for a message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": received int " + i);
		}
		return i;
	}

	public void sendDouble(double d) throws FatalChannelException {
		// Analogous to sendString but for double.

		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": sending double " + d);
		}
		try {
			out.writeDouble(d);
			out.flush(); // Don't forget to flush the output stream after
							// writing!
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while sending the message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public double receiveDouble() throws ConnectionTimeoutException,
			FatalChannelException {
		// Analogous to receiveString but for double.

		double d = 0.0;
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": waiting for a double ...");
		}
		try {
			d = in.readDouble();
		} catch (java.net.SocketTimeoutException e) {
			// The read() operation waited longer than timeout milliseconds
			// without receiving anything. This is not necessarily a fatal
			// error so we throw a ConnectionTimeoutException to the caller
			// of this method that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection timeout");
			}
			ConnectionTimeoutException ce = new ConnectionTimeoutException(e);
			throw (ce);
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while waiting for a message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": received double " + d);
		}
		return d;
	}

	public void sendBytes(byte[] b) throws FatalChannelException {
		// Analogous to sendString but for byte arrays.

		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": sending " + b.length + " bytes");
		}
		try {
			out.write(b);
			out.flush(); // Don't forget to flush the output stream after
							// writing!
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while sending the message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public byte[] receiveBytes(int size) throws ConnectionTimeoutException,
			FatalChannelException {
		// Analogous to receiveString but for byte arrays.
		byte[] b = new byte[size];
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": waiting for " + size + " bytes ...");
		}
		try {
			// Notice the asymmetry with read of individual bytes: the
			// array is given as argument to readFully
			in.readFully(b);
		} catch (java.net.SocketTimeoutException e) {
			// The read() operation waited longer than timeout milliseconds
			// without receiving anything. This is not necessarily a fatal
			// error so we throw a ConnectionTimeoutException to the caller
			// of this method that can catch it and take some suitable action.
			if (displayDebuggingMessages) {
				System.out.println("     Channel " + name1 + "/" + name2
						+ ": connection timeout");
			}
			ConnectionTimeoutException ce = new ConnectionTimeoutException(e);
			throw (ce);
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while waiting for a message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": received " + b.length + " bytes");
		}
		return b;
	}

	public void sendPacket(Packet p) throws FatalChannelException {
		// // Analogous to sendString but for objects of type Packet.

		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": sending packet " + p);
		}
		try {
			out.writeObject(p);
			out.flush(); // Don't forget to flush the output stream after
							// writing!
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while sending the message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	public Packet receivePacket() throws FatalChannelException {
		// Analogous to receiveString but for objects of type Packet

		Packet packet = null;
		if (displayDebuggingMessages) {
			System.out.println("     Channel " + name1 + "/" + name2
					+ ": waiting for a packet ...");
		}
		try {
			packet = (Packet) (in.readObject());
			if (packet != null) {
				if (displayDebuggingMessages) {
					System.out.println("     Channel " + name1 + "/" + name2
							+ ": received packet " + packet);
				}
			}

		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while waiting for a message.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
		return packet;

	}

	

	// ----------------------------------------------------------------------
	// Close method:
	// ----------------------------------------------------------------------

	public void close() throws FatalChannelException {
		// This method closes the channel by closing the two in/out
		// streams and the two sockets. If there is a problem, a
		// FatalChannelException is thrown.
		try {
			in.close();
			out.close();
			socket.close();
		} catch (Exception e) {
			System.out
					.println("Channel: an error occurred while closing the channel.");
			e.printStackTrace();
			FatalChannelException ce = new FatalChannelException(e);
			throw (ce);
		}
	}

	/**
	 * This method returns the local IP address, i.e., the IP address associated
	 * with the local end of this connection. Use this to obtain your IP address
	 * rather than
	 * 
	 * InetAddress addr = InetAddress.getLocalHost(); peerAddress =
	 * addr.getHostAddress();
	 * 
	 * because the latter does not work properly on certain linux boxes.
	 * 
	 * @return The local IP address
	 */
	public String getLocalIPAddress() {
		InetAddress localAddress = socket.getLocalAddress();
		return localAddress.getHostAddress();
	}
}
